/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.context.embedded.tomcat;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.naming.NamingException;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.naming.ContextBindings;
import org.springframework.boot.context.embedded.EmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerException;
import org.springframework.util.Assert;

/**
 * {@link EmbeddedServletContainer} that can be used to control an embedded Tomcat server.
 * Usually this class should be created using the
 * {@link TomcatEmbeddedServletContainerFactory} and not directly.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @since 1.0.0
 * @see TomcatEmbeddedServletContainerFactory
 */
public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer {

	private static final Log logger = LogFactory.getLog(TomcatEmbeddedServletContainer.class);

	private static final AtomicInteger containerCounter = new AtomicInteger(-1);

	private final Object monitor = new Object();

	private final Map<Service, Connector[]> serviceConnectors = new HashMap<Service, Connector[]>();

	private final Tomcat tomcat;

	private final boolean autoStart;

	private volatile boolean started;

	/**
	 * Create a new {@link TomcatEmbeddedServletContainer} instance.
	 * @param tomcat the underlying Tomcat server
	 */
	public TomcatEmbeddedServletContainer(Tomcat tomcat) {
		this(tomcat, true);
	}

	/**
	 * Create a new {@link TomcatEmbeddedServletContainer} instance.
	 * @param tomcat the underlying Tomcat server
	 * @param autoStart if the server should be started
	 */
	public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		// 初始化嵌入式Tomcat容器，并启动Tomcat
		initialize();
	}

	/**initialize方法完成了以下几个工作：
	 * <br/>1、重命名tomcat引擎名称
	 * <br/>2、对Context添加生命周期监听事件
	 * <br/>3、启动服务触发初始化监听
	 * <br/>4、检查TomcatStarter对象是否存在及Container状态是否正确
	 * <br/>5、绑定命名到类加载器
	 * <br/>6、启动守护等待线程
	 * @throws EmbeddedServletContainerException
	 */
	private void initialize() throws EmbeddedServletContainerException {
		TomcatEmbeddedServletContainer.logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				// 将实例id添加到tomcat引擎名字中，格式为“原引擎名字 - 实例id”
				addInstanceIdToEngineName();
				try {
					// 从tomcat的host中获得子context
					final Context context = findContext();
					// 添加生命周期的监听事件
					context.addLifecycleListener(new LifecycleListener() {

						@Override
						public void lifecycleEvent(LifecycleEvent event) {
							if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
								// 移除connector，确保当服务器启动时不会进行协议绑定
								removeServiceConnectors();
							}
						}

					});

					// Start the server to trigger initialization listeners
					// 启动tomcat，主要是利用LifecycleBase对这一套容器（engine，host，context及wrapper）进行启动并发布诸如configure_start、before_init、after_start的lifecycleEvent事件给相应的监听器（如果有的话）。进入start，因为此时状态是LifecycleState.NEW，所以会执行init方法
					this.tomcat.start();

					// We can re-throw failure exception directly in the main thread
					// 这个方法检查初始化过程中的异常，如果有直接在主线程抛出，检查方法是TomcatStarter中的private volatile Exception startUpException，这个值是在Context启动过程中记录的
					rethrowDeferredStartupExceptions();

					try {
						// 绑定命名的上下文和classloader，不成功也无所谓
						ContextBindings.bindClassLoader(context, getNamingToken(context), getClass().getClassLoader());
					}
					catch (NamingException ex) {
						// Naming is not enabled. Continue
					}

					// 与Jetty不同，Tomcat所有的线程都是守护线程，所以创建一个非守护线程（例：Thread[container-0,5,main]）来避免服务到这就shutdown了
					startDaemonAwaitThread();
				}
				catch (Exception ex) {
					containerCounter.decrementAndGet();
					throw ex;
				}
			}
			catch (Exception ex) {
				stopSilently();
				throw new EmbeddedServletContainerException("Unable to start embedded Tomcat", ex);
			}
		}
	}

	private Context findContext() {
		for (Container child : this.tomcat.getHost().findChildren()) {
			if (child instanceof Context) {
				return (Context) child;
			}
		}
		throw new IllegalStateException("The host does not contain a Context");
	}

	private void addInstanceIdToEngineName() {
		int instanceId = containerCounter.incrementAndGet();
		if (instanceId > 0) {
			Engine engine = this.tomcat.getEngine();
			engine.setName(engine.getName() + "-" + instanceId);
		}
	}

	/**将上面new的connection以service（这里是StandardService[Tomcat]）做key保存到private final Map<Service, Connector[]> serviceConnectors中，
	 * <br/>并将StandardService中的protected Connector[] connectors与service解绑(connector.setService((Service)null);)，解绑后下面利用LifecycleBase启动容器就不会启动到Connector了
	 */
	private void removeServiceConnectors() {
		for (Service service : this.tomcat.getServer().findServices()) {
			Connector[] connectors = service.findConnectors().clone();
			this.serviceConnectors.put(service, connectors);
			for (Connector connector : connectors) {
				service.removeConnector(connector);
			}
		}
	}

	/**这个方法检查初始化过程中的异常，如果有直接在主线程抛出，检查方法是TomcatStarter中的private volatile Exception startUpException，这个值是在Context启动过程中记录的
	 * @throws Exception
	 */
	private void rethrowDeferredStartupExceptions() throws Exception {
		Container[] children = this.tomcat.getHost().findChildren();
		for (Container container : children) {
			if (container instanceof TomcatEmbeddedContext) {
				Exception exception = ((TomcatEmbeddedContext) container).getStarter().getStartUpException();
				if (exception != null) {
					throw exception;
				}
			}
			if (!LifecycleState.STARTED.equals(container.getState())) {
				throw new IllegalStateException(container + " failed to start");
			}
		}
	}

	/**可以看到，spring-boot应用在启动的过程中，由于默认启动了Tomcat暴露HTTP服务，所以执行到了上述方法，而Tomcat启动的所有的线程，默认都是daemon线程，例如监听请求的Acceptor，工作线程池等等，
	 * <br/>如果这里不加控制的话，启动完成之后JVM也会退出。因此需要显示的启动一个线程，在某个条件下进行持续等待，从而避免线程退出。
	 *
	 */
	private void startDaemonAwaitThread() {
		Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

			@Override
			public void run() {
				TomcatEmbeddedServletContainer.this.tomcat.getServer().await();
			}

		};
		awaitThread.setContextClassLoader(getClass().getClassLoader());
		awaitThread.setDaemon(false);
		awaitThread.start();
	}

	@Override
	public void start() throws EmbeddedServletContainerException {
		synchronized (this.monitor) {
			if (this.started) {
				return;
			}
			try {
				// 将内置容器创建和初始化阶段删除的Connector再添加到容器，将Connector添加回容器(实际上是添加到容器的Service)，
				// 因为相应的Service已经处于启动状态，所以Connector在添加回来之后马上会被启动
				addPreviouslyRemovedConnectors();
				// 获得tomcat的Connector，如果不为空并且设置为自动启动，则启动之。缺省配置下，
				// 这里 autoStart 为 true
				// 连接器 Connector 主要是接收用户的请求，然后封装请求传递给容器处理，tomcat中默认的连接器是Coyote。
				// 连接器表示Tomcat将会在哪个端口使用哪种协议提供服务。
				// 在配置文件中，我们经常会见到这样的配置 :
				// <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="utf-8"/>
				// <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
				Connector connector = this.tomcat.getConnector();
				if (connector != null && this.autoStart) {
					performDeferredLoadOnStartup();
				}
				// 检查确保Connector已经启动，如果没启动，抛出异常
				checkThatConnectorsHaveStarted();
				this.started = true;
				TomcatEmbeddedServletContainer.logger.info("Tomcat started on port(s): " + getPortsDescription(true));
			}
			catch (ConnectorStartFailedException ex) {
				stopSilently();
				throw ex;
			}
			catch (Exception ex) {
				throw new EmbeddedServletContainerException("Unable to start embedded Tomcat servlet container", ex);
			}
			finally {
				Context context = findContext();
				ContextBindings.unbindClassLoader(context, getNamingToken(context), getClass().getClassLoader());
			}
		}
	}

	private void checkThatConnectorsHaveStarted() {
		for (Connector connector : this.tomcat.getService().findConnectors()) {
			if (LifecycleState.FAILED.equals(connector.getState())) {
				throw new ConnectorStartFailedException(connector.getPort());
			}
		}
	}

	private void stopSilently() {
		try {
			stopTomcat();
		}
		catch (LifecycleException ex) {
			// Ignore
		}
	}

	private void stopTomcat() throws LifecycleException {
		if (Thread.currentThread().getContextClassLoader() instanceof TomcatEmbeddedWebappClassLoader) {
			Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
		}
		this.tomcat.stop();
	}

	private void addPreviouslyRemovedConnectors() {
		Service[] services = this.tomcat.getServer().findServices();
		for (Service service : services) {
			Connector[] connectors = this.serviceConnectors.get(service);
			if (connectors != null) {
				for (Connector connector : connectors) {
					// 此时service已经处于启动状态，因此重新添加进来的connector马上被执行启动动作
					service.addConnector(connector);
					if (!this.autoStart) {
						stopProtocolHandler(connector);
					}
				}
				this.serviceConnectors.remove(service);
			}
		}
	}

	private void stopProtocolHandler(Connector connector) {
		try {
			connector.getProtocolHandler().stop();
		}
		catch (Exception ex) {
			TomcatEmbeddedServletContainer.logger.error("Cannot pause connector: ", ex);
		}
	}

	private void performDeferredLoadOnStartup() {
		try {
			for (Container child : this.tomcat.getHost().findChildren()) {
				if (child instanceof TomcatEmbeddedContext) {
					((TomcatEmbeddedContext) child).deferredLoadOnStartup();
				}
			}
		}
		catch (Exception ex) {
			TomcatEmbeddedServletContainer.logger.error("Cannot start connector: ", ex);
			throw new EmbeddedServletContainerException("Unable to start embedded Tomcat connectors", ex);
		}
	}

	Map<Service, Connector[]> getServiceConnectors() {
		return this.serviceConnectors;
	}

	@Override
	public void stop() throws EmbeddedServletContainerException {
		synchronized (this.monitor) {
			boolean wasStarted = this.started;
			try {
				this.started = false;
				try {
					stopTomcat();
					this.tomcat.destroy();
				}
				catch (LifecycleException ex) {
					// swallow and continue
				}
			}
			catch (Exception ex) {
				throw new EmbeddedServletContainerException("Unable to stop embedded Tomcat", ex);
			}
			finally {
				if (wasStarted) {
					containerCounter.decrementAndGet();
				}
			}
		}
	}

	private String getPortsDescription(boolean localPort) {
		StringBuilder ports = new StringBuilder();
		for (Connector connector : this.tomcat.getService().findConnectors()) {
			ports.append((ports.length() != 0) ? " " : "");
			int port = (localPort ? connector.getLocalPort() : connector.getPort());
			ports.append(port + " (" + connector.getScheme() + ")");
		}
		return ports.toString();
	}

	@Override
	public int getPort() {
		Connector connector = this.tomcat.getConnector();
		if (connector != null) {
			return connector.getLocalPort();
		}
		return 0;
	}

	/**
	 * Returns access to the underlying Tomcat server.
	 * @return the Tomcat server
	 */
	public Tomcat getTomcat() {
		return this.tomcat;
	}

	private Object getNamingToken(Context context) {
		try {
			return context.getNamingToken();
		}
		catch (NoSuchMethodError ex) {
			// Use the context itself on Tomcat 7
			return context;
		}

	}

}
